# -*- coding: utf-8 -*-
# Copyright (c) 2015, Maxwell Morais and contributors
# For license information, please see license.txt

from __future__ import unicode_literals

import frappe
from frappe.utils import cstr, encode
import os
import sys
import inspect
import traceback
import linecache
import pydoc
import cgitb
import datetime
import json
import six

def make_error_snapshot(exception):
	if frappe.conf.disable_error_snapshot:
		return

	logger = frappe.logger(__name__, with_more_info=False)

	try:
		error_id = '{timestamp:s}-{ip:s}-{hash:s}'.format(
			timestamp=cstr(datetime.datetime.now()),
			ip=frappe.local.request_ip or '127.0.0.1',
			hash=frappe.generate_hash(length=3)
		)
		snapshot_folder = get_error_snapshot_path()
		frappe.create_folder(snapshot_folder)

		snapshot_file_path = os.path.join(snapshot_folder, "{0}.json".format(error_id))
		snapshot = get_snapshot(exception)

		with open(encode(snapshot_file_path), 'wb') as error_file:
			error_file.write(encode(frappe.as_json(snapshot)))

		logger.error('New Exception collected with id: {}'.format(error_id))

	except Exception as e:
		logger.error('Could not take error snapshot: {0}'.format(e), exc_info=True)

def get_snapshot(exception, context=10):
	"""
	Return a dict describing a given traceback (based on cgitb.text)
	"""

	etype, evalue, etb = sys.exc_info()
	if isinstance(etype, six.class_types):
		etype = etype.__name__

	# creates a snapshot dict with some basic information

	s = {
		'pyver': 'Python {version:s}: {executable:s} (prefix: {prefix:s})'.format(
			version = sys.version.split()[0],
			executable = sys.executable,
			prefix = sys.prefix
		),
		'timestamp': cstr(datetime.datetime.now()),
		'traceback': traceback.format_exc(),
		'frames': [],
		'etype': cstr(etype),
		'evalue': cstr(repr(evalue)),
		'exception': {},
		'locals': {}
	}

	# start to process frames
	records = inspect.getinnerframes(etb, 5)

	for frame, file, lnum, func, lines, index in records:
		file = file and os.path.abspath(file) or '?'
		args, varargs, varkw, locals = inspect.getargvalues(frame)
		call = ''

		if func != '?':
			call = inspect.formatargvalues(args, varargs, varkw, locals, formatvalue=lambda value: '={}'.format(pydoc.text.repr(value)))

		# basic frame information
		f = {'file': file, 'func': func, 'call': call, 'lines': {}, 'lnum': lnum}

		def reader(lnum=[lnum]):
			try:
				return linecache.getline(file, lnum[0])
			finally:
				lnum[0] += 1

		vars = cgitb.scanvars(reader, frame, locals)

		# if it is a view, replace with generated code
		# if file.endswith('html'):
		# 	lmin = lnum > context and (lnum - context) or 0
		# 	lmax = lnum + context
		# 	lines = code.split("\n")[lmin:lmax]
		# 	index = min(context, lnum) - 1

		if index is not None:
			i = lnum - index
			for line in lines:
				f['lines'][i] = line.rstrip()
				i += 1

		# dump local variable (referenced in current line only)
		f['dump'] = {}
		for name, where, value in vars:
			if name in f['dump']:
				continue
			if value is not cgitb.__UNDEF__:
				if where == 'global':
					name = 'global {name:s}'.format(name=name)
				elif where != 'local':
					name = where + ' ' + name.split('.')[-1]
				f['dump'][name] = pydoc.text.repr(value)
			else:
				f['dump'][name] = 'undefined'

		s['frames'].append(f)

	# add exception type, value and attributes
	if isinstance(evalue, BaseException):
		for name in dir(evalue):
			# prevent py26 DeprecationWarning
			if (name != 'messages' or sys.version_info < (2.6)) and not name.startswith('__'):
				value = pydoc.text.repr(getattr(evalue, name))

				# render multilingual string properly
				if type(value)==str and value.startswith(b"u'"):
					value = eval(value)

				s['exception'][name] = encode(value)

	# add all local values (of last frame) to the snapshot
	for name, value in locals.items():
		if type(value)==str and value.startswith(b"u'"):
			value = eval(value)

		s['locals'][name] = pydoc.text.repr(value)

	return s

def collect_error_snapshots():
	"""Scheduled task to collect error snapshots from files and push into Error Snapshot table"""
	if frappe.conf.disable_error_snapshot:
		return

	try:
		path = get_error_snapshot_path()
		if not os.path.exists(path):
			return

		for fname in os.listdir(path):
			fullpath = os.path.join(path, fname)

			try:
				with open(fullpath, 'r') as filedata:
					data = json.load(filedata)

			except ValueError:
				# empty file
				os.remove(fullpath)
				continue

			for field in ['locals', 'exception', 'frames']:
				data[field] = frappe.as_json(data[field])

			doc = frappe.new_doc('Error Snapshot')
			doc.update(data)
			doc.save()

			frappe.db.commit()

			os.remove(fullpath)

		clear_old_snapshots()

	except Exception as e:
		make_error_snapshot(e)

		# prevent creation of unlimited error snapshots
		raise

def clear_old_snapshots():
	"""Clear snapshots that are older than a month"""
	frappe.db.sql("""delete from `tabError Snapshot`
		where creation < date_sub(now(), interval 1 month)""")

	path = get_error_snapshot_path()
	today = datetime.datetime.now()

	for file in os.listdir(path):
		p = os.path.join(path, file)
		ctime = datetime.datetime.fromtimestamp(os.path.getctime(p))
		if (today - ctime).days > 31:
			os.remove(os.path.join(path, p))

def get_error_snapshot_path():
	return frappe.get_site_path('error-snapshots')
